//模板编译
class Compile {
  //节点和实例
  constructor(el, vm) {
    //el有可能是字符串'#app'也有可能是document.querySelector获取的
    this.el = this.isElementNode(el) ? el : document.querySelector(el);
    this.vm = vm;

    // console.log(el); '#app'

    if (this.el) {
      //拿到<div id="app">...</div>底下的所有节点
      let docFragment = this.nodeToFragment(this.el);

      /**
       * console.log(docFragment);
       * #document-frament
       *   <input type="text">
       *   "{{message}}"
       */

      //编译
      this.compile(docFragment);

      //将编译好的节点插入el节点渲染到页面
      this.el.appendChild(docFragment);

    }

  }

  //判断是不是元素节点
  isElementNode(node) {
    //返回元素节点 true/false
    return node.nodeType === 1;
  }

  //将节点转为文档碎片
  nodeToFragment(el) {
    let docFragment = document.createDocumentFragment(el);

    //<div id="app">...</div>底下的节点可能有很多
    let firstChild;
    //直到为false才停止
    //当匹配到最后一个才停止
    //el.firstChild => el底下的第一个元素
    //当找不到最后一个时 => firstChild = null => while循环中止
    while (firstChild = el.firstChild) {
      docFragment.appendChild(firstChild);
    }
    return docFragment;

  }

  //编译文档碎片
  compile(docFragment) {
    //拿到子节点
    let childNodes = docFragment.childNodes;
    // console.log(childNodes);
    //NodeList(3) [text, input, text]

    //将类数组转为数组并循环拿到每一个节点
    Array.from(childNodes).forEach(node => {
      if (this.isElementNode(node)) {
        //元素节点

        //标签嵌套里 有可能遇到元素节点
        //标签嵌套里 有可能遇到文本节点
        //此时就要递归
        this.compile(node);

        // console.log('元素节点', node);

        //编译元素节点
        this.compileElement(node);

      } else {
        //文本节点
        // console.log('文本节点', node);

        //编译文本节点
        this.compileText(node);

      }
    });
  }

  //编译元素节点
  compileElement(node) {

    //当遇到v-model="message"属性时进行编译
    //拿到v-model
    let attrs = node.attributes;

    //例子：<input type="text" v-model="message">

    /**
     * console.log(attrs);
     * NamedNodeMap {
     *   0: type,
     *   1: v-model,
     *   type: type,
     *   v-model: v-model,
     *   length: 2
     * }
     */

    //类数组转为数组并遍历每一项
    Array.from(attrs).forEach(attr => {
      // console.log(attr);
      //type="text"
      //v-model="message"

      //console.log(attr.name); type / v-model
      //console.log(attr.value); text / message

      //找出v-指令
      let attrName = attr.name;
      //判断是否是v-指令
      if (this.isDirective(attrName)) {
        // console.log(attrName);
        //v-model

        let value = attr.value;
        // console.log(value);
        //拿到v-model="message"里面的message
        //message
        //注意：这里的message对应vue里data里的message属性

        //拿到v-model 的model 去除v-
        //从第2位截取到最后
        let type = attrName.slice(2);
        //console.log(type);
        //model 

        //执行CompileUtils里面的方法
        //传入节点，实例，表达式
        CompileUtils[type](node, this.vm, value);

      }


    });

  }

  //编译文本节点
  compileText(node) {
    /**
     *   <div id="app">
     *     <input type="text" v-model="message">
     *     {{message}}
     *     <ul>
     *     <li>haha</li>
     *     </ul>
     *   </div>
     */

    // console.log(node);
    //#text/"{{message}}"/#text/"haha"/#text/#text

    //拿到文本节点的内容
    let text = node.textContent;
    //console.log(text);
    //{{message}}
    //haha

    //匹配{{}} 排除中间 带}
    let reg = /\{\{([^}]+)\}\}/g
    if (reg.test(text)) {
      //这里的message对应vue里data里的message属性
      //执行CompileUtils里面的方法
      //传入节点，实例，表达式
      CompileUtils['text'](node, this.vm, text);
    }

  }

  //是否是v-指令 接收属性名参数
  isDirective(attrName) {
    return attrName.includes('v-');
  }

}

CompileUtils = {
  //一旦model() 就触发 modelUpdater()
  /**
   * 调用底下updater函数的函数
   * @param {*} node 节点
   * @param {*} vm 实例
   * @param {*} exp 表达式
   */
  model(node, vm, exp) {
    let updateFn = this.updater['modelUpdater'];
    //data里属性的值是表达式exp => value 即 vm.$data[exp]
    //value有可能存在嵌套的情况 如 message: {a:'hello'}
    //嵌套时候调用的写法顺序：是vm.$data.message.a 而不是vm.$data['message.a']

    //将模板编译和数据劫持关联起来的监听器
    //this.cb(newValue) => ()=>{}
    new Watcher(vm, exp, () => {
      //该方法存在的情况并执行
      updateFn && updateFn(node, this.getVal(vm, exp));
    });

    node.addEventListener('input', (e) => {
      let newValue = e.target.value;
      this.setValue(vm, exp, newValue);
    });

    updateFn && updateFn(node, this.getVal(vm, exp));

  },
  text(node, vm, exp) {
    // console.log(exp);
    //{{message.a}}

    //去掉{{}}
    let reg = /\{\{([^}]+)\}\}/g;

    //该方法存在的情况并执行
    let updateFn = this.updater['textUpdater'];

    // console.log(exp);
    //{{message.a}}

    let value = this.getTextValue(vm, exp);

    // console.log(value);
    //Hello world!

    exp.replace(reg, (...args) => {
      // console.log(exp);
      //message.a

      /**
       * console.log(args);
       * [
       *   0: "{{message.a}}",
       *   1: "message.a",
       *   2: 5,
       *   3: "\n    {{message.a}}\n    ",
       *   length: 4
       * ]
       */

      // console.log(this.getTextValue(vm, exp));
      //Hello world!

      new Watcher(vm, args[1], () => {
        updateFn && updateFn(node, this.getTextValue(vm, exp));
      });
    });

    // console.log(value);

    updateFn && updateFn(node, value);
  },
  //负责拿到['message.a']的值
  //实现 message message.a message.a.b message.a.b.c的调用的值
  getVal(vm, exp) {
    // console.log(exp);
    //message.a

    //按照.进行拆分
    exp = exp.split('.');
    // console.log(exp);
    //['message', 'a']

    //这里返回 'Hello world!'
    return exp.reduce((prev, next) => {
      // console.log(prev);
      // console.log(vm.$data);

      //{a: 'Hello world!'}
      return prev[next];
    }, vm.$data);

  },
  updater: {
    modelUpdater(node, value) {
      // console.log(value);
      //'Hello world!'
      node.value = value;
    },
    textUpdater(node, value) {
      // console.log(value);
      node.textContent = value;
    }
  },
  //处理文本表达式里面带{{}}内容
  getTextValue(vm, exp) {
    // console.log(exp);
    //{{message.a}}

    //去掉{{}}
    let reg = /\{\{([^}]+)\}\}/g;
    let value = exp.replace(reg, (...args) => {

      // console.log(args[1]);
      // message.a

      // console.log(this.getVal(vm, args[1]));
      //'Hello world!'

      return this.getVal(vm, args[1]);
    });

    return value;
  },
  setValue(vm, exp, newValue) {
    //按照.进行拆分
    exp = exp.split('.');
    // console.log(exp);
    //['message', 'a']

    //这里返回 'Hello world!'
    return exp.reduce((prev, next, currentIndex) => {
      // console.log(prev);
      // console.log(vm.$data);

      //{a: 'Hello world!'}
      if (currentIndex === exp.length - 1) {
        return prev[next] = newValue;
      }

      return prev[next];
    }, vm.$data);
  }
}